export const {
gettext, ngettext, pgettext,
} = Extension.defineTranslationFunctions();
+
+export class InjectionManager {
+ #savedMethods = new Map();
+
+ /**
+ * @callback CreateOverrideFunc
+ * @param {Function?} originalMethod - the original method if it exists
+ * @returns {Function} - a function to be used as override
+ */
+
+ /**
+ * Modify, replace or inject a method
+ *
+ * @param {object} prototype - the object (or prototype) that is modified
+ * @param {string} methodName - the name of the overwritten method
+ * @param {CreateOverrideFunc} createOverrideFunc - function to call to create the override
+ */
+ overrideMethod(prototype, methodName, createOverrideFunc) {
+ const originalMethod = this._saveMethod(prototype, methodName);
+ prototype[methodName] = createOverrideFunc(originalMethod);
+ }
+
+ /**
+ * Restore the original method
+ *
+ * @param {object} prototype - the object (or prototype) that is modified
+ * @param {string} methodName - the name of the method to restore
+ */
+ restoreMethod(prototype, methodName) {
+ const savedProtoMethods = this.#savedMethods.get(prototype);
+ if (!savedProtoMethods)
+ return;
+
+ const originalMethod = savedProtoMethods.get(methodName);
+ if (originalMethod === undefined)
+ delete prototype[methodName];
+ else
+ prototype[methodName] = originalMethod;
+
+ savedProtoMethods.delete(methodName);
+ if (savedProtoMethods.size === 0)
+ this.#savedMethods.delete(prototype);
+ }
+
+ /**
+ * Restore all original methods and clear overrides
+ */
+ clear() {
+ for (const [proto, map] of this.#savedMethods) {
+ map.forEach(
+ (_, methodName) => this.restoreMethod(proto, methodName));
+ }
+ console.assert(this.#savedMethods.size === 0,
+ `${this.#savedMethods.size} overrides left after clear()`);
+ }
+
+ _saveMethod(prototype, methodName) {
+ let savedProtoMethods = this.#savedMethods.get(prototype);
+ if (!savedProtoMethods) {
+ savedProtoMethods = new Map();
+ this.#savedMethods.set(prototype, savedProtoMethods);
+ }
+
+ const originalMethod = prototype[methodName];
+ savedProtoMethods.set(methodName, originalMethod);
+ return originalMethod;
+ }
+}
--- /dev/null
+const JsUnit = imports.jsUnit;
+
+import '../../js/ui/environment.js';
+
+import {InjectionManager} from '../../js/extensions/extension.js';
+
+class Object1 {
+ count = 0;
+
+ getNumber() {
+ return 42;
+ }
+
+ getCount() {
+ return ++this.count;
+ }
+}
+
+/**
+ * @param {object} object to modify
+ */
+function addInjections(object) {
+ // extend original method
+ injectionManager.overrideMethod(
+ object, 'getNumber', originalMethod => {
+ return function () {
+ // eslint-disable-next-line no-invalid-this
+ const num = originalMethod.call(this);
+ return 2 * num;
+ };
+ });
+
+ // override original method
+ injectionManager.overrideMethod(
+ object, 'getCount', () => {
+ return function () {
+ return 42;
+ };
+ });
+
+ // inject new method
+ injectionManager.overrideMethod(
+ object, 'getOtherNumber', () => {
+ return function () {
+ return 42;
+ };
+ });
+}
+
+
+const injectionManager = new InjectionManager();
+let obj;
+
+// Prototype injections
+addInjections(Object1.prototype);
+
+obj = new Object1();
+
+// new obj is modified
+JsUnit.assertEquals(obj.getNumber(), 84);
+JsUnit.assertEquals(obj.getCount(), 42);
+JsUnit.assertEquals(obj.count, 0);
+JsUnit.assertEquals(obj.getOtherNumber(), 42);
+
+injectionManager.clear();
+
+obj = new Object1();
+
+// new obj is unmodified
+JsUnit.assertEquals(obj.getNumber(), 42);
+JsUnit.assertEquals(obj.getCount(), obj.count);
+JsUnit.assert(obj.count > 0);
+JsUnit.assertRaises(() => obj.getOtherNumber());
+
+// instance injections
+addInjections(obj);
+
+// obj is now modified
+JsUnit.assertEquals(obj.getNumber(), 84);
+JsUnit.assertEquals(obj.getCount(), 42);
+JsUnit.assertEquals(obj.count, 1);
+JsUnit.assertEquals(obj.getOtherNumber(), 42);
+
+injectionManager.restoreMethod(obj, 'getNumber');
+JsUnit.assertEquals(obj.getNumber(), 42);
+
+injectionManager.clear();
+
+// obj is unmodified again
+JsUnit.assertEquals(obj.getNumber(), 42);
+JsUnit.assertEquals(obj.getCount(), obj.count);
+JsUnit.assert(obj.count > 0);
+JsUnit.assertRaises(() => obj.getOtherNumber());